﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Mime;
using System.Text;
using POP.Mime.Decode;
using POP.Mime.Header;
using POP.Common;

namespace POP.Mime
{
	// A MessagePart is a part of an email message used to describe the whole email parse tree.<br/>
	public class MessagePart
	{
		#region Public properties
		// The Content-Type header field.
		public ContentType ContentType { get; private set; }

		// A human readable description of the body
		public string ContentDescription { get; private set; }

		// This header describes the Content encoding during transfer.
		public ContentTransferEncoding ContentTransferEncoding { get; private set; }

		// ID of the content part (like an attached image). Used with MultiPart messages.
		public string ContentId { get; private set; }

		public ContentDisposition ContentDisposition { get; private set; }

		public Encoding BodyEncoding { get; private set; }

		// This is the parsed body of this <see cref="MessagePart"/>.<br/>
		// It is parsed in that way, if the body was ContentTransferEncoded, it has been decoded to the
		public byte[] Body { get; private set; }

		// Describes if this <see cref="MessagePart"/> is a MultiPart message<br/>
		public bool IsMultiPart
		{
			get
			{
				return ContentType.MediaType.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase);
			}
		}

		public bool IsText
		{
			get
			{
				string mediaType = ContentType.MediaType;
				return mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) || mediaType.Equals("message/rfc822", StringComparison.OrdinalIgnoreCase);
			}
		}

		// - it has a Content-Disposition header that says it is an attachment
		public bool IsAttachment
		{
			get
			{
				// Inline is the opposite of attachment
				return (!IsText && !IsMultiPart) || (ContentDisposition != null && !ContentDisposition.Inline);
			}
		}

		// This is a convenient-property for figuring out a FileName for this <see cref="MessagePart"/>.<br/>
		// If the <see cref="MessagePart"/> is a MultiPart message, then it makes no sense to try to find a FileName.<br/>
		// <br/>
		// The FileName can be specified in the <see cref="ContentDisposition"/> or in the <see cref="ContentType"/> properties.<br/>
		// If none of these places two places tells about the FileName, a default "(no name)" is returned.
		public string FileName { get; private set; }

		// If this <see cref="MessagePart"/> is a MultiPart message, then this property
		// has a list of each of the Multiple parts that the message consists of.<br/>
		// <br/>
		// It is <see langword="null"/> if it is not a MultiPart message.<br/>
		// Use <see cref="IsMultiPart"/> to check if this <see cref="MessagePart"/> is a MultiPart message.
		public List<MessagePart> MessageParts { get; private set; }
		#endregion

		#region Constructors
		// Used to construct the topmost message part
		internal MessagePart(byte[] rawBody, MessageHeader headers)
		{
			if(rawBody == null)
				throw new ArgumentNullException("rawBody");
			
			if(headers == null)
				throw new ArgumentNullException("headers");

			ContentType = headers.ContentType;
			ContentDescription = headers.ContentDescription;
			ContentTransferEncoding = headers.ContentTransferEncoding;
			ContentId = headers.ContentId;
			ContentDisposition = headers.ContentDisposition;

			FileName = FindFileName(ContentType, ContentDisposition, "(no name)");
			BodyEncoding = ParseBodyEncoding(ContentType.CharSet);

			ParseBody(rawBody);
		}
		#endregion

		#region Parsing
		// Parses a character set into an encoding
		private static Encoding ParseBodyEncoding(string characterSet)
		{
			// Default encoding in Mime messages is US-ASCII
			Encoding encoding = Encoding.ASCII;

			// If the character set was specified, find the encoding that the character
			// set describes, and use that one instead
			if (!string.IsNullOrEmpty(characterSet))
				encoding = EncodingFinder.FindEncoding(characterSet);

			return encoding;
		}

		// Figures out the filename of this message part from some headers.
		private static string FindFileName(ContentType contentType, ContentDisposition contentDisposition, string defaultName)
		{
			if(contentType == null)
				throw new ArgumentNullException("contentType");

			if (contentDisposition != null && contentDisposition.FileName != null)
				return contentDisposition.FileName;

			if (contentType.Name != null)
				return contentType.Name;

			return defaultName;
		}

		// Parses a byte array as a body of an email message.
		private void ParseBody(byte[] rawBody)
		{
			if(IsMultiPart)
			{
				// Parses a MultiPart message
				ParseMultiPartBody(rawBody);
			} else
			{
				// Parses a non MultiPart message
				// Decode the body accodingly and set the Body property
				Body = DecodeBody(rawBody, ContentTransferEncoding);
			}
		}

		// Parses the <paramref name="rawBody"/> byte array as a MultiPart message.<br/>
		// It is not valid to call this method if <see cref="IsMultiPart"/> returned <see langword="false"/>.<br/>
		// Fills the <see cref="MessageParts"/> property of this <see cref="MessagePart"/>.
		private void ParseMultiPartBody(byte[] rawBody)
		{
			// Fetch out the boundary used to delimit the messages within the body
			string multipartBoundary = ContentType.Boundary;

			// Fetch the individual MultiPart message parts using the MultiPart boundary
			List<byte[]> bodyParts = GetMultiPartParts(rawBody, multipartBoundary);

			// Initialize the MessageParts property, with room to as many bodies as we have found
			MessageParts = new List<MessagePart>(bodyParts.Count);

			// Now parse each byte array as a message body and add it the the MessageParts property
			foreach (byte[] bodyPart in bodyParts)
			{
				MessagePart messagePart = GetMessagePart(bodyPart);
				MessageParts.Add(messagePart);
			}
		}

		// Given a byte array describing a full message.<br/>
		// Parses the byte array into a <see cref="MessagePart"/>.
		private static MessagePart GetMessagePart(byte[] rawMessageContent)
		{
			// Find the headers and the body parts of the byte array
			MessageHeader headers;
			byte[] body;
			HeaderExtractor.ExtractHeadersAndBody(rawMessageContent, out headers, out body);

			// Create a new MessagePart from the headers and the body
			return new MessagePart(body, headers);
		}

		// Gets a list of byte arrays where each entry in the list is a full message of a message part
		private static List<byte[]> GetMultiPartParts(byte[] rawBody, string multipPartBoundary)
		{
			if(rawBody == null)
				throw new ArgumentNullException("rawBody");

			// This is the list we want to return
			List<byte[]> messageBodies = new List<byte[]>();

			// Create a stream from which we can find MultiPart boundaries
			using (MemoryStream stream = new MemoryStream(rawBody))
			{
				bool lastMultipartBoundaryEncountered;

				// Find the start of the first message in this multipart
				// Since the method returns the first character on a the line containing the MultiPart boundary, we
				// need to add the MultiPart boundary with prepended "--" and appended CRLF pair to the position returned.
				int startLocation = FindPositionOfNextMultiPartBoundary(stream, multipPartBoundary, out lastMultipartBoundaryEncountered) + ("--" + multipPartBoundary + "\r\n").Length;
				while (true)
				{
					// When we have just parsed the last multipart entry, stop parsing on
					if(lastMultipartBoundaryEncountered)
						break;

					// Find the end location of the current multipart
					// Since the method returns the first character on a the line containing the MultiPart boundary, we
					// need to go a CRLF pair back, so that we do not get that into the body of the message part
					int stopLocation = FindPositionOfNextMultiPartBoundary(stream, multipPartBoundary, out lastMultipartBoundaryEncountered) - "\r\n".Length;

					// If we could not find the next multipart boundary, but we had not yet discovered the last boundary, then
					// we will consider the rest of the bytes as contained in a last message part.
					if (stopLocation <= -1)
					{
						// Include everything except the last CRLF.
						stopLocation = (int) stream.Length - "\r\n".Length;

						// We consider this as the last part
						lastMultipartBoundaryEncountered = true;

						// Special case: when the last multipart delimiter is not ending with "--", but is indeed the last
						// one, then the next multipart would contain nothing, and we should not include such one.
						if (startLocation >= stopLocation)
							break;
					}

					// We have now found the start and end of a message part
					// Now we create a byte array with the correct length and put the message part's bytes into
					// it and add it to our list we want to return
					int length = stopLocation - startLocation;
					byte[] messageBody = new byte[length];
					Array.Copy(rawBody, startLocation, messageBody, 0, length);
					messageBodies.Add(messageBody);

					// We want to advance to the next message parts start.
					// We can find this by jumping forward the MultiPart boundary from the last
					// message parts end position
					startLocation = stopLocation + ("\r\n" + "--" + multipPartBoundary + "\r\n").Length;
				}
			}

			// We are done
			return messageBodies;
		}

		// Method that is able to find a specific MultiPart boundary in a Stream.
		// The Stream passed should not be used for anything else then for looking for MultiPart boundaries
		private static int FindPositionOfNextMultiPartBoundary(Stream stream, string multiPartBoundary, out bool lastMultipartBoundaryFound)
		{
			lastMultipartBoundaryFound = false;
			while(true)
			{
				// Get the current position. This is the first position on the line - no characters of the line will
				// have been read yet
				int currentPos = (int) stream.Position;

				// Read the line
				string line = StreamUtility.ReadLineAsAscii(stream);

				// If we kept reading until there was no more lines, we did not meet
				// the MultiPart boundary. -1 is then returned to describe this.
				if (line == null)
					return -1;

				// The MultiPart boundary is the MultiPartBoundary with "--" in front of it
				// which is to be at the very start of a line
				if (line.StartsWith("--" + multiPartBoundary, StringComparison.Ordinal))
				{
					// Check if the found boundary was also the last one
					lastMultipartBoundaryFound = line.StartsWith("--" + multiPartBoundary + "--", StringComparison.OrdinalIgnoreCase);
					return currentPos;
				}
			}
		}

		// Decodes a byte array into another byte array based upon the Content Transfer encoding
		private static byte[] DecodeBody(byte[] messageBody, ContentTransferEncoding contentTransferEncoding)
		{
			if (messageBody == null)
				throw new ArgumentNullException("messageBody");

			switch (contentTransferEncoding)
			{
				case ContentTransferEncoding.QuotedPrintable:
					// If encoded in QuotedPrintable, everything in the body is in US-ASCII
					return QuotedPrintable.DecodeContentTransferEncoding(Encoding.ASCII.GetString(messageBody));

				case ContentTransferEncoding.Base64:
					// If encoded in Base64, everything in the body is in US-ASCII
					return Base64.Decode(Encoding.ASCII.GetString(messageBody));

				case ContentTransferEncoding.SevenBit:
				case ContentTransferEncoding.Binary:
				case ContentTransferEncoding.EightBit:
					// We do not have to do anything
					return messageBody;

				default:
					throw new ArgumentOutOfRangeException("contentTransferEncoding");
			}
		}
		#endregion

		#region Public methods
		// Gets this MessagePart's <see cref="Body"/> as text.<br/>
		// This is simply the <see cref="BodyEncoding"/> being used on the raw bytes of the <see cref="Body"/> property.<br/>
		// This method is only valid to call if it is not a MultiPart message and therefore contains a body.<br/>
		public string GetBodyAsText()
		{
			return BodyEncoding.GetString(Body);
		}

		// Save this <see cref="MessagePart"/>'s contents to a file.<br/>
		// There are no methods to reload the file.
		public void Save(FileInfo file)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			using (FileStream stream = new FileStream(file.FullName, FileMode.Create))
			{
				Save(stream);
			}
		}

		// Save this <see cref="MessagePart"/>'s contents to a stream.<br/>
		public void Save(Stream messageStream)
		{
			if (messageStream == null)
				throw new ArgumentNullException("messageStream");

			messageStream.Write(Body, 0, Body.Length);
		}
		#endregion
	}
}